package script

import (
	"context"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

// 构造对象
func NewExcutor(moduleDir string) *Excutor {
	return &Excutor{
		moduleDir: moduleDir,
	}
}

// 1. 寻找脚本, 定义一个脚本存放的目录位置
// 2. 调用脚本
// 负责执行脚本(模块)
// 执行:   脚本的名称(模块名称), 脚本的参数
// Script Excutor 需要在一个目录下面 搜索脚本, 搜索目录需要提前定义
type Excutor struct {
	// 脚本存放的目录位置
	moduleDir string
}

func NewExecRequest() *ExecRequest {
	return &ExecRequest{
		Output: os.Stdout,
	}
}

type ExecRequest struct {
	// 执行参数
	// 模块的名称(moduleName): 脚本名称
	ModuleName string
	// 模块的参数(moduleParams): 字符串
	ModuleParams string
	// 返回的结果, 以Io流的方式直接写回去
	// 默认输出到标准输出
	Output io.Writer
}

func (req *ExecRequest) Validate() error {
	if req.ModuleName == "" {
		return fmt.Errorf("module name required")
	}

	return nil
}

// 定义接口
func (e *Excutor) Exec(ctx context.Context, in *ExecRequest) error {
	if err := in.Validate(); err != nil {
		return err
	}

	// 寻找模块
	moduleAbsPath, err := e.FindModule(in.ModuleName)
	if err != nil {
		return err
	}

	log.Printf("find module abs path: %s", moduleAbsPath)

	// 执行模块
	// 使用 os/exec 来执行系统命令
	// 根据脚本的扩展名, 来决定如何执行
	var cmd *exec.Cmd
	// 2.1 构造cmd命令
	ext := filepath.Ext(in.ModuleName)
	switch ext {
	case ".sh":
		cmd = exec.Command("bash", moduleAbsPath, in.ModuleParams)
	case ".py":
		cmd = exec.Command("python", moduleAbsPath, in.ModuleParams)
	default:
		// 二进制可执行文件 直接执行
		cmd = exec.Command(moduleAbsPath, in.ModuleParams)
	}

	// 如何执行命令, Run就是前台阻塞运行
	// Start, 后台执行, 不等待命令返回结果? 结果怎么获取,设置输出参数
	cmd.Stdout = in.Output
	cmd.Stderr = in.Output
	if err := cmd.Start(); err != nil {
		return err
	}

	// 等待命令执行结束
	return cmd.Wait()
}

// 寻找脚本, 定义一个脚本存放的目录位置:  modules的绝对路径+文件名
// 需要找到脚本执行的绝对路径: /modules/test.sh , ../../../sh rm -rf ./*, test/../../../../sh
// 安全问题: 避免脚本寻找抛出modules的目录
func (e *Excutor) FindModule(moduleName string) (string, error) {
	// 获取到存放脚本目录的 绝对路径
	modulePath, err := filepath.Abs(e.moduleDir)
	if err != nil {
		return "", fmt.Errorf("find module %s abs path error %s", modulePath, err)
	}
	// 防止用户传入的执行脚本 超出指定目录
	if strings.Contains(moduleName, "..") {
		return "", fmt.Errorf("module forbiden .. in module")
	}

	// 拼凑脚本绝对路径
	return filepath.Join(modulePath, moduleName), nil
}
